package cs340.stormtroopers.hypeerweb;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.SortedSet;
import java.util.TreeSet;

/**
 * Represents the connections a given node has to other nodes. </br>
 * 
 * <pre>
 * <b>Instance Domain:</b>
 * 		neighbors:                           List&lt;Node&gt;
 * 		upPointers:                          List&lt;Node&gt;
 * 		downPointers:                        List&lt;Node&gt;
 * 		fold:                                Node
 * 		surrogateFold:                       Node
 * 		inverseSurrogateFold:                Node
 * 
 * <b>Instance Variants</b>
 * 		The child of a node (referenced later in this Javadoc) has a webId that differs by exactly
 * 			one bit from the parent's webId, and has a height exactly 1 greater than the parent's
 * 			webId. (Specifics for adding children is included in the Javadoc in the Node class.)
 * 		neighbors is a list of nodes that satisfy the following conditions:
 * 			-The list exists throughout the lifetime of this node
 * 			-The webId's of these nodes differ by only one bit from the WebId of this node
 * 		upPointers is a list of nodes
 * 			-This list exists throughout the lifetime of this node
 * 			-This list is not empty if and only if this node does not have a child.
 * 			-The webId of each node in this list differs by only 1 bit from the child webId of this node.
 * 			-->(That is to say, this is a list of all the neighbors a child node would have if it was created from this node.)
 * 		downPointers is a list of nodes
 * 			-This list exists throughout the lifetime of this node.
 * 			-The nodes in this list have no child nodes
 * 			-Their child webId's of these nodes differ by only one bit from this node's webId.
 * 			-This list is not empty if and only if this node is an edge node.
 * 				-->Edge nodes are nodes that have been added to a perfect hypercube, and thus are
 * 					"pointing down" to nodes contained within the perfect hypercube portion of
 * 					the HyPeerWeb.
 * 		fold is the node whose webId is the binary complement of this node's webId.
 * 		surrogateFold is a node that satisfies the following conditions: 
 * 			-Is not null if and only if no node exists as fold for this node
 * 			-The webId of the surrogateFold is the parent of the webId obtained by taking
 * 				the binary complement of this node's webId.
 * 		inverseSurrogateFold is a node that satisfies the following conditions:
 * 			-This node does not have a child.
 * 			-The webId of the inverseSurrogateFold is the binary complement of the child webId
 * 				of this Node's webId.
 * </pre>
 * 
 * 
 * @author Andy Morrow
 * @author Brad Spendlove
 * @author Daniel Howard
 * @author James Gurr
 * 
 */

public class Connections implements Serializable, Iterable<Node>
{
	/**
	 * 
	 */
	private static final long serialVersionUID = -3632399083548765517L;
	private TreeSet<Node> neighbors;
	private ArrayList<Node> upPointers;
	private ArrayList<Node> downPointers;
	private Node fold;
	private Node surrogateFold;
	private Node inverseSurrogateFold;

	public Connections()
	{
		neighbors = new TreeSet<Node>();
		upPointers = new ArrayList<Node>();
		downPointers = new ArrayList<Node>();
		fold = Node.NULL_NODE;
		surrogateFold = Node.NULL_NODE;
		inverseSurrogateFold = Node.NULL_NODE;
	}
	
	public Connections(Node fold, Node surrogateFold, Node inverseSurrogateFold)
	{
		neighbors = new TreeSet<Node>();
		upPointers = new ArrayList<Node>();
		downPointers = new ArrayList<Node>();
		this.fold = fold;
		this.surrogateFold = surrogateFold;
		this.inverseSurrogateFold = inverseSurrogateFold;
	}

	/**
	 * Public constructor creates the connections calculated for this node in
	 * the node's constructor.
	 * 
	 * @param neighbors
	 * 			  list of all nodes whose webId's are neighbors of this node's webId
	 * @param upPointers
	 *            list of all neighbors of this node's child's webId.
	 * @param downPointers
	 *            list of all parents of neighboring webId's for which a node
	 *            has not yet been created.
	 * @param fold
	 *            binary complement of this node's webId.
	 * @param surrogateFold
	 *            the parent of this webId's fold if the webId for the fold has
	 *            no node.
	 * @param inverseSurrogateFold
	 *            the fold of this webId's child webId if no node has been
	 *            created for it yet.
	 * 
	 * @pre <pre>
	 * 		neighbors is not null AND</br>
	 * 		upPointers is not null AND</br>
	 * 		downPointers is not null</br>
	 * </pre>
	 * 
	 * @post <pre>
	 * 		neighbors = new list of neighbors to this node AND</br>
	 * 		upPointers = new list of nodes AND</br>
	 * 		downPointers = new list of nodes AND</br>
	 * 		fold = the fold calculated for this node AND</br>
	 * 		surrogateFold = the surrogateFold calculated for this node AND</br>
	 * 		inverseSurrogateFold = the inverseSurrogateFold calculated for this node</br>
	 * 
	 * </pre>
	 */
	public Connections(TreeSet<Node> neighbors, ArrayList<Node> upPointers,
			ArrayList<Node> downPointers, Node fold, Node surrogateFold,
			Node inverseSurrogateFold)
	{
		// These MUST be copy constructors because the original lists are
		// subject to change.
		// DON'T CHANGE THIS TO SIMPLE ASSIGNMENT!
		this.neighbors = new TreeSet<Node>(neighbors);
		this.upPointers = new ArrayList<Node>(upPointers);
		this.downPointers = new ArrayList<Node>(downPointers);
		this.fold = fold;
		this.surrogateFold = surrogateFold;
		this.inverseSurrogateFold = inverseSurrogateFold;
	}

	public Connections(Connections other)
	{
		neighbors = new TreeSet<Node>(other.neighbors);
		upPointers = new ArrayList<Node>(other.upPointers);
		downPointers = new ArrayList<Node>(other.downPointers);
		fold = other.fold;
		surrogateFold = other.surrogateFold;
		inverseSurrogateFold = other.inverseSurrogateFold;
	}

	/**
	 * Returns the <i>inverseSurrogateFold</i>. Also known as the
	 * <i>inverseSurrogateFold</i> getter.
	 * 
	 * @return inverseSurrogateFold
	 * 
	 * @pre <i>None</i>
	 * @post result = inverseSurrogateFold
	 */
	public Node getInverseSurrogateFold()
	{
		return inverseSurrogateFold;
	}

	/**
	 * Returns the <i>surrogateFold</i>. Also known as the <i>surrogateFold</i>
	 * getter.
	 * 
	 * @return surrogateFold
	 * 
	 * @pre <i>None</i>
	 * @post result = surrogateFold
	 */
	public Node getSurrogateFold()
	{
		return surrogateFold;
	}

	/**
	 * Returns the <i>fold</i>. Also known as the <i>fold</i> getter.
	 * 
	 * @return fold
	 * 
	 * @pre <i>None</i>
	 * @post result = fold
	 */
	public Node getFold()
	{
		return fold;
	}

	/**
	 * Returns the <i>neighbors</i>. Also known as the <i>neighbors</i> getter.
	 * 
	 * @pre <i>None</i>
	 * @post result = neighbors
	 */
	public TreeSet<Node> getNeighbors()
	{
		return neighbors;
	}

	/**
	 * Return the <i>upPointers</i>. Also known as the <i>upPointers</i> getter.
	 * 
	 * @return upPointers
	 * 
	 * @pre <i>None</i>
	 * @post result = upPointers
	 */
	public ArrayList<Node> getUpPointers()
	{
		return upPointers;
	}

	/**
	 * Returns the <i>downPointers</i>. Also known as the <i>downPointers</i>
	 * getter.
	 * 
	 * @return downPointers
	 * 
	 * @pre <i>None</i>
	 * @post result = downPointers
	 */
	public ArrayList<Node> getDownPointers()
	{
		return downPointers;
	}

	/**
	 * Returns all neighboring nodes whose webId's are bigger than this node's webId.
	 * 
	 * @pre <i>None</i>
	 * @post result = a list of neighboring nodes whose webId's are bigger than this node's webId.
	 */
	public SortedSet<Node> getNeighborsBiggerThanMe(Node me)
	{
		return neighbors.tailSet(me, false);
	}

	/**
	 * Returns a list of neighboring nodes whose webId's are smaller than this node's
	 * webId and who have children.
	 * 
	 * @pre <i>None</i>
	 * @post result = list of neighboring nodes whose webId's are smaller than this node's webId
	 * 				  and who have children.
	 */
	public SortedSet<Node> getParentNeighborsSmallerThanMe(Node me)
	{
		SortedSet<Node> ret = new TreeSet<Node>();
		for (Node n : neighbors.headSet(me))
			if (hasChild(n))
				ret.add(n);
		return ret;
	}

	/**
	 * Returns a list of neighboring nodes whose webId's are less than this node's
	 * webId and who do not have children.
	 * 
	 * @pre <i>None</i>
	 * @post result = list of neighboring nodes whose webId's are less than this node's webId
	 * 				  and who do not have children.
	 */
	public SortedSet<Node> getChildlessNeighborsSmallerThanMe(Node me)
	{
		SortedSet<Node> ret = new TreeSet<Node>();
		for (Node n : neighbors.headSet(me))
			if (!hasChild(n))
				ret.add(n);
		return ret;
	}

	private boolean hasChild(Node n)
	{
		Node dummy = new Node(n.getWebId().createChildsWebId().getValue());
		return neighbors.contains(dummy);
	}

	/**
	 * Sets the neighbors of this Connections to the TreeSet<Node> representing
	 * the neighbors
	 * 
	 * @param neighbors
	 *            List of all neighbors
	 * 
	 * @pre neighbors &ne; null
	 * @post this.neighbors = neighbors
	 */
	public void setNeighbors(TreeSet<Node> neighbors)
	{
		this.neighbors = new TreeSet<Node>(neighbors);
	}

	/**
	 * Sets the childlessNeighborsSmallerThanMe of this Connections to the
	 * ArrayList<Node> representing the childlessNeighborsSmallerThanMe.
	 * 
	 * @param childlessNeighborsSmallerThanMe
	 * 
	 * @pre childlessNeighborsSmallerThanMe &ne; null
	 * @post this.childlessNeighborsSmallerThanMe =
	 *       childlessNeighborsSmallerThanMe
	 */
	// public void setChildlessNeighborsSmallerThanMe(
	// ArrayList<Node> childlessNeighborsSmallerThanMe)
	// {
	// this.childlessNeighborsSmallerThanMe = childlessNeighborsSmallerThanMe;
	// }

	/**
	 * Sets the parentNeighborsSmallerThanMe of this Connections to the
	 * ArrayList<Node> representing the parentNeighborsSmallerThanMe.
	 * 
	 * @param parentNeighborsSmallerThanMe
	 * 
	 * @pre parentNeighborsSmallerThenMe &ne; null
	 * @post this.parentNeighborsSmallerThanMe = parentNeighborsSmallerThanMe
	 */
	// public void setParentNeighborsSmallerThanMe(
	// ArrayList<Node> parentNeighborsSmallerThanMe)
	// {
	// this.parentNeighborsSmallerThanMe = parentNeighborsSmallerThanMe;
	// }

	/**
	 * Sets the upPointers of this Connections to the ArrayList<Node>
	 * representing the upPointers.
	 * 
	 * @param upPointers
	 * 
	 * @pre upPointers &ne; null
	 * @post this.upPointers = upPointers
	 */
	public void setUpPointers(ArrayList<Node> upPointers)
	{
		this.upPointers = upPointers;
	}

	/**
	 * Sets the downPointers of this Connections to the ArrayList<Node>
	 * representing the downPointers.
	 * 
	 * @param downPointers
	 * 
	 * @pre downPointers &ne; null
	 * @post this.downPointers = downPointers
	 */
	public void setDownPointers(ArrayList<Node> downPointers)
	{
		this.downPointers = downPointers;
	}

	/**
	 * Sets the fold of this Connections to Node representing the fold
	 * 
	 * @param fold
	 * 
	 * @pre <i>None</i>
	 * @post this.fold = fold
	 */
	public void setFold(Node fold)
	{
		this.fold = fold;
	}

	/**
	 * Sets the surrogateFold of this Connections to Node representing the
	 * surrogateFold
	 * 
	 * @param surrogateFold
	 * 
	 * @pre <i>None</i>
	 * @post this.surrogateFold = surrogateFold
	 */
	public void setSurrogateFold(Node surrogateFold)
	{
		this.surrogateFold = surrogateFold;
	}

	/**
	 * Sets the inverseSurrogateFold of this Connections to Node representing
	 * the inverseSurrogateFold
	 * 
	 * @param inverseSurrogateFold
	 * 
	 * @pre <i>None</i>
	 * @post this.inverseSurrogateFold = inverseSurrogateFold
	 */
	public void setInverseSurrogateFold(Node inverseSurrogateFold)
	{
		this.inverseSurrogateFold = inverseSurrogateFold;
	}

	/**
	 * Adds a neighboring node to the list of neighbors
	 * 
	 * @param neighbor
	 *            the node we are going to add as a new neighbor
	 * 
	 * @pre neighbor &ne; null
	 * @post neighbors = neighbors + neighbor
	 */
	public void addNeighbor(Node neighbor)
	{
		neighbors.add(neighbor);
	}

	/**
	 * Adds a new downPointer
	 * 
	 * @param downPointer
	 *            the downPointer to be added to the list
	 * 
	 * @pre downPointer &ne; null
	 * @post result = downPointers + downPointer
	 */
	public void addDownPointer(Node downPointer)
	{
		downPointers.add(downPointer);
	}

	/**
	 * Removes the specified downPointer from the list of downPointers
	 * 
	 * @param downPointer
	 *            the downPointer to be removed from the list
	 * 
	 * @pre downPointer &ne; null
	 * @post result = downPointers - downPointer if downPointer is not found in
	 *       downPointers, no changes occur
	 * 
	 */
	public void removeDownPointer(Node downPointer)
	{
//		System.out.println("The downPointingNode to be removed has WebId: " + downPointer.getWebIdValue());
		downPointers.remove(downPointer);
//		System.out.println("Now the downPointers are: " + downPointersToString());

	}

	/**
	 * Adds a new upPointer
	 * 
	 * @param upPointer
	 *            the upPointer to be added
	 * 
	 * @pre upPointer &ne; null
	 * @post result = upPointers - upPointer if upPointer is not found in
	 *       upPointers, no changes occur
	 */
	public void addUpPointer(Node upPointer)
	{
		upPointers.add(upPointer);
	}

	/**
	 * Removes the neighbor from the list of neighbors
	 * 
	 * @param neighbor
	 *            the neighboring node to be removed
	 * 
	 * @pre neighbor &ne; null
	 * @post result = neighbors - neighbor<br/>
	 * 				  if neighbor is not found in neighbors, then no changes occur.
	 */
	public void removeNeighbor(Node neighbor)
	{
//		System.out.println("The neighborNode to be removed has WebId: " + neighbor.getWebIdValue());
		neighbors.remove(neighbor);
//		System.out.println("Now the Neighbors are: " + neighborsToString());
	}

	/**
	 * Removes the upPointer from the upPointers
	 * 
	 * @param upPointer
	 *            the upPointer to be removed
	 * 
	 * @pre upPointer &ne; null
	 * @post result = upPointers - upPointer if upPointer is not found in
	 *       upPointers, then no changes occur
	 */
	public void removeUpPointer(Node upPointer)
	{
//		System.out.println("The upPointingNode to be removed has WebId: " + upPointer.getWebIdValue());
		upPointers.remove(upPointer);
//		System.out.println("Now the UpPointers are: " + upPointersToString());

	}

	/**
	 * Completely empties the list of upPointers for this node. This occurs when
	 * a node has a child.
	 * 
	 * @pre <i>None</i>
	 * @post upPointers is empty
	 */
	public void clearUpPointers()
	{
		upPointers.clear();
	}

	/**
	 * Removes all connections for this object For use in clearing node for
	 * deletion
	 * 
	 * @pre <i>None</i>
	 * @post neighbors is empty AND </br>
	 * 		 upPointers is empty AND </br>
	 * 		 downPointers is empty AND </br>
	 * 		 fold == null AND </br>
	 *       surrogateFold == null AND </br>
	 *       inverseSurrogateFold == null </br>
	 */
	public void clearAll()
	{
		neighbors.clear();
		upPointers.clear();
		downPointers.clear();
		fold = Node.NULL_NODE;
		surrogateFold = Node.NULL_NODE;
		inverseSurrogateFold = Node.NULL_NODE;
	}

	/**
	 * Changes a node from the list of <i>childlessNeighborsSmallerThanMe</i> to
	 * the list of <i>parentNeighborsBiggerThanMe</i> if the node has a child
	 * node.
	 * 
	 * @param neighbor
	 *            the neighbor that had the child
	 * 
	 * @pre neighbor &ne; null AND neighbor is in the list
	 *      childlessNeighborsSmallerThanMe
	 * @post neighbor is removed from childlessNeighborsSmallerThanMe AND</br>
	 *       neighbor is added to parentNeighborsSmallerThanMe
	 * 
	 */
	// public void neighborHadChild(Node neighbor)
	// {
	// childlessNeighborsSmallerThanMe.remove(neighbor);
	// parentNeighborsSmallerThanMe.add(neighbor);
	// }

	/**
	 * Replace an old neighbor with a new one.
	 * 
	 * @param oldNeighbor
	 *            the neighbor being replaced
	 * @param newNeighbor
	 *            the new neighbor taking the old neighbor's place
	 * 
	 * @pre oldNeighbor &ne; null AND newNeighbor &ne; null
	 * @post oldNeighbor is replaced with newNeighbor in neighbors
	 */
	public void replaceNeighbor(Node oldNeighbor, Node newNeighbor)
	{
		// we use the indices to keep the lists in order
		neighbors.remove(oldNeighbor);
		neighbors.add(newNeighbor);
	}

	/**
	 * Replaces a downPointer to an old node with a downPointer to a new node
	 * 
	 * @param oldNode
	 *            the downPointer node being replaced
	 * @param newNode
	 *            the new node the new downPointer points to
	 * 
	 * @pre oldNode &ne; null AND newNode &ne; null
	 * @post oldNeighbor is replaced with newNode in downPointers
	 */
	public void replaceDownPointer(Node oldNode, Node newNode)
	{
		// keep lists in order
		int index = downPointers.indexOf(oldNode);
		if (index >= 0)
			downPointers.set(index, newNode);
	}

	/**
	 * Replaces an upPointer to an old node with an upPointer to a new node
	 * 
	 * @param oldNode
	 *            the old upPointer to the old node
	 * @param newNode
	 *            the new upPOinter to the new node
	 * 
	 * @pre oldNode &ne; null AND newNode &ne; null
	 * @post oldNode is replaced with newNode in upPointers
	 */
	public void replaceUpPointer(Node oldNode, Node newNode)
	{
		// keep lists in order
		int index = upPointers.indexOf(oldNode);
		if (index >= 0)
			upPointers.set(index, newNode);
	}

	@Override
	/**
	 * Implementation of Iterable to allow easy iteration over all the Nodes in this connections.
	 * 
	 * @pre None
	 * @post Result is an iterator over all the nodes in this connections and no others. 
	 */
	public Iterator<Node> iterator()
	{
		ArrayList<Node> allConnections = new ArrayList<Node>();
		allConnections.addAll(neighbors);
		allConnections.addAll(upPointers);
		allConnections.addAll(downPointers);
		allConnections.add(fold);
		allConnections.add(surrogateFold);
		allConnections.add(inverseSurrogateFold);

		return allConnections.iterator();
	}

	/**
	 * Debugging toString() for seeing the state of the connections
	 * 
	 * @pre None
	 * @post result = A string containing the information about the state of the
	 *       connection, including neighbors, fold, surrogate fold, and inverse
	 *       surrogate fold
	 */
	public String toString()
	{
		try
		{
			return "neighbors: " + neighborsToString() + "\nFold: "
			+ fold.getWebIdValue() + "\n SurFold: "
			+ surrogateFold.getWebIdValue() + "\n InvSurFold: "
			+ inverseSurrogateFold.getWebIdValue() + "\n upPointers: "
			+ upPointersToString() + "\n downPointers: "
			+ downPointersToString();
			
		}
		catch (NullPointerException e)
		{
			return "Exception thrown in Connections.toString()";
		}
	}
	
	private String downPointersToString()
	{
		StringBuilder sb = new StringBuilder();
		
		for(Node n : downPointers)
		{
			sb.append(n.getWebIdValue() + " ");
		}
		return sb.toString();
	}

	private String upPointersToString()
	{
		StringBuilder sb = new StringBuilder();
		for(Node n : upPointers)
		{
			sb.append(n.getWebIdValue() + " ");
		}
		return sb.toString();
	}

	/**
	 * Converts the list of neighbors into a string and returns it.
	 * 
	 * @return ret the string value of the neighbors list
	 * 
	 * @pre None
	 * @post result = (String) neighbors
	 */
	private String neighborsToString(){
		StringBuilder sb = new StringBuilder();
		for(Node n: neighbors)
			sb.append(n.getWebIdValue()+" ");
		return sb.toString();
	}

	/**
	 * Calculates the connections a child node to parent will have and returns them.
	 * 
	 * @param parent the node we are adding a child to
	 * @return childConnections the list of connections the child node has
	 * 
	 * @pre parent &ne; null
	 * @post result = set of all connections the child node will have (details are listed)
	 * 				  in the javadoc comment for node.getChildConnections)
	 */
	public Connections getChildConnections(Node parent)
	{
		// this is from my class notes: "child's fold is parent's old fold,
		// unless parent has an invSurrFold
		Node childfold;
		if (getInverseSurrogateFold() != Node.NULL_NODE)
			childfold = getInverseSurrogateFold();
		else
			childfold = getFold();

		TreeSet<Node> newneighbors = new TreeSet<Node>();
		newneighbors.addAll(upPointers); // "childlessNeighborsSmallerThanMe"
		newneighbors.add(parent);

		ArrayList<Node> neighborsBiggerThanMe = new ArrayList<Node>(
				getNeighborsBiggerThanMe(parent));
		Connections childConnections = new Connections(newneighbors, // parentNeighborsSmallerThanMe
				new ArrayList<Node>(), // upPointers
				neighborsBiggerThanMe, // downPointers
				childfold, // fold
				Node.NULL_NODE, // surrogateFold
				Node.NULL_NODE // inverseSurrogateFold
		);
		
//		System.out.println("These are the new child's connections " + childConnections.toString());
		
		return childConnections;
	}
}
